S259: VSCode Extension Native Git Operations Tab
Documentation for Plans
S259: VSCode Extension — Native Git Operations Tab
Session: S259.3 Status: Plan — awaiting approval Priority: High — brings full git visibility into the IDE for all platform users Depends on: S254 (clone URLs), S251 (git-operations library)
Summary
Add a "Repositories" tab to the Vitto VSCode extension that natively renders the git-graph, branch sidebar, file browser, commit list, server metrics, and clone-to-workspace flow — all powered by the existing @libs-structural/git-operations library. Role-based access controls which repos a user sees and what actions they can perform.
Architecture
Data Flow
Extension Webview (Lit)
↓ postMessage
Extension Host (Node.js)
↓ HTTP (Authorization: Bearer JWT)
backend-ai-api:3001
├── GET /ai-api/git/repos → native fetch → sandbox-orchestrator /git/repos
├── GET /ai-api/git/repos/:s/:u/* → native fetch → sandbox-orchestrator /git/repos/:s/:u/*
└── spaceId resolution → HttpService → backend-api:3000 /api/spaces/:id
Why This Shape
- Extension talks to one backend (ai-api). No infra topology leaks.
- ai-api already has
SANDBOX_SERVER_URL+SANDBOX_API_KEY(SelfHostedProvider) andBACKEND_API_URL+BACKEND_API_INTERNAL_TOKEN(PlatformOrchestrationService). Zero new env vars. - JWT already carries
platformRole+spaceId. Role resolution and space filtering are free — no extra HTTP calls from the extension. - Pure-TS utils import directly into the extension via esbuild path aliases (already configured in
esbuild.mjs). No Angular dependency.
What Gets Reused vs. Rewritten
| Layer | Reuse | Rewrite |
|---|---|---|
Types (git-operations.types.ts) |
Direct import | — |
Role config (git-operations-role.config.ts) |
Direct import | — |
| Template paths config | Direct import | — |
Format utils (shortenSha, timeAgo, etc.) |
Direct import | — |
Lane assignment (assignLanes, computeCommitAge) |
Direct import | — |
SVG renderer (generateRowSvg, computeSvgWidth) |
Direct import | — |
| 7 Angular components | — | Port to Lit |
GitOperationsApiService |
— | Replace with extension host HTTP handler |
GitOperationsStateService |
— | Replace with Lit reactive state in webview |
UxPortalService (settings modal) |
— | Replace with Lit dialog |
ContextMenuDirective |
— | Replace with vanilla JS right-click |
Backend: GitProxyController + GitProxyService
New Files
APIs/backend-ai-api/src/app/git-proxy/
├── git-proxy.module.ts
├── git-proxy.controller.ts
└── git-proxy.service.ts
GitProxyService
Follows SelfHostedProvider pattern exactly:
@Injectable()
export class GitProxyService implements OnModuleInit {
private baseUrl: string | null = null;
private apiKey: string | null = null;
private backendApiUrl: string;
private systemToken: string;
constructor(
private readonly config: ConfigService,
private readonly httpService: HttpService,
) {}
onModuleInit() {
// Orchestrator (native fetch)
this.baseUrl = this.config.get<string>('SANDBOX_SERVER_URL', '') || null;
this.apiKey = this.config.get<string>('SANDBOX_API_KEY', '') || null;
// Backend-api (HttpService)
this.backendApiUrl = this.config.get<string>('BACKEND_API_URL') || 'http://localhost:3000/api';
this.systemToken = this.config.get<string>('BACKEND_API_INTERNAL_TOKEN')
|| this.config.get<string>('SYSTEM_TOKEN') || '';
}
// Proxy to orchestrator: native fetch + X-API-Key
async listRepos(): Promise<GitRepoInfo[]> { ... }
async getRepoBranches(space, universe): Promise<GitBranchInfo[]> { ... }
async getRepoCommits(space, universe, ref, limit): Promise<GitCommitInfo[]> { ... }
async getCommitDiff(space, universe, sha): Promise<GitCommitDiff> { ... }
async getFileTree(space, universe, ref): Promise<GitFileTreeItem[]> { ... }
async getFileContent(space, universe, filePath, ref): Promise<GitFileContent> { ... }
async getCloneUrls(space, universe): Promise<{ ssh, https, repoPath }> { ... }
async getHostMetrics(): Promise<HetznerHostMetrics | null> { ... }
// Resolve spaceId → spaceSlug via backend-api
async resolveSpaceSlug(spaceId: string): Promise<string | null> { ... }
}
GitProxyController
@Controller('git')
@UseGuards(UnifiedAuthGuard)
export class GitProxyController {
// GET /ai-api/git/repos
// - Reads req.user.platformRole → resolveGitOpsTier()
// - If user tier: resolves req.user.spaceId → spaceSlug, filters repos
// - If admin+: returns all repos
// - Attaches roleTier + features to response
// GET /ai-api/git/repos/:space/:universe/branches
// GET /ai-api/git/repos/:space/:universe/commits
// GET /ai-api/git/repos/:space/:universe/commits/:sha
// GET /ai-api/git/repos/:space/:universe/tree
// GET /ai-api/git/repos/:space/:universe/file
// GET /ai-api/git/repos/:space/:universe/clone-urls
// GET /ai-api/git/metrics
// - All validate space access based on role tier
// - Forward to orchestrator via GitProxyService
}
Space Access Validation
private async validateSpaceAccess(caller: UnifiedCaller, spaceSlug: string): Promise<boolean> {
const tier = resolveGitOpsTier(caller.platformRole);
if (['executive', 'developer', 'admin'].includes(tier)) return true;
// User tier: resolve their spaceId → slug, compare
const userSpaceSlug = await this.gitProxyService.resolveSpaceSlug(caller.spaceId);
return userSpaceSlug === spaceSlug;
}
Response Shape for /ai-api/git/repos
{
repos: GitRepoInfo[], // filtered by role
roleTier: GitOpsRoleTier, // 'executive' | 'developer' | 'admin' | 'user'
features: GitOpsFeatureFlags // full feature flag set for this tier
}
Registration
Add GitProxyModule to AppModule.imports in app.module.ts. Module imports HttpModule + ConfigModule.
Extension Host: Git Operations Handler
New File
apps/vitto-vscode-extension/src/services/git-operations.handler.ts
Responsibilities
- HTTP client to
ai-api/git/*endpoints - Uses
ConfigService.getBackendUrl()for base URL - Uses same auth header construction as
McpClientService.getAuthHeaders() - Methods mirror the controller endpoints
- Handles the
gitCloneToWorkspacemessage from webview:- Check if repo already cloned (scan workspace folder git remotes)
- If found: offer to add to workspace via
vscode.workspace.updateWorkspaceFolders() - If not:
vscode.commands.executeCommand('git.clone', url)
Message Types (added to types.ts)
// Webview → Extension Host
| { type: 'gitListRepos' }
| { type: 'gitSelectRepo'; space: string; universe: string }
| { type: 'gitSwitchBranch'; space: string; universe: string; branch: string }
| { type: 'gitGetCommitDiff'; space: string; universe: string; sha: string }
| { type: 'gitOpenFile'; space: string; universe: string; filePath: string; ref: string }
| { type: 'gitCloneToWorkspace'; url: string }
| { type: 'gitGetCloneUrls'; space: string; universe: string }
| { type: 'gitGetMetrics' }
// Extension Host → Webview
| { type: 'gitReposLoaded'; repos: GitRepoInfo[]; roleTier: string; features: GitOpsFeatureFlags }
| { type: 'gitRepoSelected'; branches: GitBranchInfo[]; files: GitFileTreeItem[]; commits: GitCommitInfo[]; allCommits: GitCommitInfo[]; cloneUrls: { ssh: string; https: string; repoPath: string } }
| { type: 'gitBranchSwitched'; files: GitFileTreeItem[]; commits: GitCommitInfo[] }
| { type: 'gitCommitDiffLoaded'; diff: GitCommitDiff }
| { type: 'gitFileContentLoaded'; content: string; path: string }
| { type: 'gitCloneUrlsLoaded'; ssh: string; https: string; repoPath: string }
| { type: 'gitMetricsLoaded'; metrics: HetznerHostMetrics }
| { type: 'gitError'; message: string }
Extension Webview: "Repositories" Tab
New Files
apps/vitto-vscode-extension/src/panels/webview/components/repositories/
├── repositories-view.ts # Main tab view (orchestrator)
├── repo-card.ts # Individual repo card
├── repo-detail.ts # Detail view (graph + files + commits)
├── git-graph-lit.ts # Git graph (SVG, lane legend, clone dropdown)
├── branch-sidebar-lit.ts # Branch list + CRUD
├── file-browser-lit.ts # File tree grouped by directory
├── commit-list-lit.ts # Flat commit list
├── commit-detail-panel-lit.ts # Slide panel with diff stats
├── server-metrics-lit.ts # Metrics strip (gated by role)
└── context-menu-lit.ts # Shared vanilla JS context menu
Tab Registration
- types.ts: Add
'repositories'toTabIdunion - app.ts: Add import, tab entry (
{ id: 'repositories', label: 'Repositories', icon: 'source-control' }), and render slot - state.ts: Add git operations state fields to
AppState - api.ts: Add message handlers and API functions
State Shape (added to AppState)
// Git operations state
gitRepos: GitRepoInfo[];
gitRoleTier: GitOpsRoleTier;
gitFeatures: GitOpsFeatureFlags | null;
gitSelectedRepo: GitRepoInfo | null;
gitBranches: GitBranchInfo[];
gitFiles: GitFileTreeItem[];
gitCommits: GitCommitInfo[]; // current branch
gitAllCommits: GitCommitInfo[]; // --all (for graph)
gitCloneUrls: { ssh: string; https: string; repoPath: string } | null;
gitSelectedBranch: string;
gitDetailTab: 'graph' | 'files' | 'commits';
gitExpandedCommitSha: string | null;
gitCommitDiff: GitCommitDiff | null;
gitFileContent: string | null;
gitSelectedFilePath: string | null;
gitLoading: boolean;
gitDetailLoading: boolean;
gitHostMetrics: HetznerHostMetrics | null;
gitSearchQuery: string;
gitError: string | null;
Component Hierarchy (mirrors gograb-dev page exactly)
repositories-view
├── [overview mode: !selectedRepo]
│ ├── server-metrics-lit (gated: features.showServerMetrics)
│ ├── stats strip (totalRepos, totalBranches, totalFiles)
│ ├── search input
│ └── repo-card grid (filtered by search + role)
│ └── repo-card × N
│ ├── space/universe header
│ ├── branch count, file count, last commit
│ └── delete button (gated: features.canDeleteRepos)
│
└── [detail mode: selectedRepo]
├── back button
├── tab bar: Graph | Files | Commits
├── branch-sidebar-lit (left)
│ ├── branch list with switch/merge/delete actions
│ └── create branch form (gated: features.canManageBranches)
└── main content area
├── [graph tab]
│ └── git-graph-lit
│ ├── lane legend (branch pills, focus on click)
│ ├── clone dropdown (SSH/HTTPS copy + "Clone to Workspace" button)
│ ├── settings popover (show all branches, author, timestamp)
│ └── SVG graph rows (importable generateRowSvg)
├── [files tab]
│ └── file-browser-lit
│ ├── template sources toggle (gated: features.canViewTemplateSources)
│ └── grouped file list with icons
└── [commits tab]
└── commit-list-lit
├── flat commit list
└── inline expandable diff
Clone-to-Workspace Flow (Inside Git Graph)
The clone dropdown lives inside git-graph-lit, matching the existing Angular component's design.
UX Flow
1. User clicks "Clone ▾" button in git-graph header
2. Dropdown shows:
┌────────────────────────────────────────────┐
│ SSH │
│ ┌──────────────────────────────┬────────┐ │
│ │ [email protected]:... │ Copy │ │
│ └──────────────────────────────┴────────┘ │
│ HTTPS │
│ ┌──────────────────────────────┬────────┐ │
│ │ https://sandbox.gograb... │ Copy │ │
│ └──────────────────────────────┴────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ ↓ Clone to Workspace │ │
│ └─────────────────────────────────────┘ │
│ Opens VSCode's native git clone dialog │
└────────────────────────────────────────────┘
3. "Copy" → navigator.clipboard.writeText(url)
4. "Clone to Workspace" → postMessage({ type: 'gitCloneToWorkspace', url: sshUrl })
→ Extension host:
a) Scan open workspace folders for matching git remote URL
b) If found: vscode.window.showInformationMessage(
'Already cloned at /path', 'Add to Workspace', 'Open Folder')
c) If not: vscode.commands.executeCommand('git.clone', url)
→ Native VSCode clone: folder picker → progress → auto-open
Smart Detection (Extension Host)
async function findExistingClone(remoteUrl: string): Promise<string | null> {
const gitExtension = vscode.extensions.getExtension('vscode.git');
if (!gitExtension) return null;
const git = gitExtension.exports.getAPI(1);
for (const repo of git.repositories) {
for (const remote of repo.state.remotes) {
if (remote.fetchUrl === remoteUrl || remote.pushUrl === remoteUrl) {
return repo.rootUri.fsPath;
}
}
}
return null;
}
VSCode Theme Integration
All colors use --vscode-* CSS custom properties instead of hardcoded Catppuccin values:
| Hardcoded | VSCode Var |
|---|---|
#11111b (BG) |
--vscode-editor-background |
#cdd6f4 (text) |
--vscode-editor-foreground |
#313244 (border) |
--vscode-panel-border |
#1e1e2e (surface) |
--vscode-sideBar-background |
#f38ba8 (error) |
--vscode-errorForeground |
#a6e3a1 (success) |
--vscode-testing-iconPassed |
Lane colors (LANE_COLORS) are kept as-is — they need to be distinct and consistent regardless of theme. The SVG renderer's BG constant is parameterized to use the CSS var value.
Role-Based Access Matrix
| Feature | Executive | Developer | Admin | User |
|---|---|---|---|---|
| See all repos (all spaces) | Yes | Yes | Yes | No (own space only) |
| Git graph | Yes | Yes | Yes | Yes |
| Branch CRUD | Yes | Yes | Yes | No |
| Merge branches | Yes | Yes | No | No |
| Delete repos | Yes | Yes | No | No |
| View files | Yes | Yes | Yes | Yes |
| Commit files | Yes | Yes | No | No |
| Server metrics | Yes | Yes | No | No |
| Template sources toggle | Yes | Yes | No | No |
| Clone to workspace | Yes | Yes | Yes | Yes |
| Commit history | Yes | Yes | Yes | No |
Implementation Order
Phase 1: Backend Proxy (ai-api)
- Create
git-proxy.service.ts— native fetch to orchestrator + space resolution - Create
git-proxy.controller.ts— REST endpoints with role-based filtering - Create
git-proxy.module.ts— register in AppModule - Verify: curl test all endpoints with JWT
Phase 2: Extension Host Handler
- Create
git-operations.handler.ts— HTTP client to ai-api/git/* - Add message types to
types.ts - Wire message handlers in
main-panel.ts - Add clone-to-workspace logic (git API integration)
Phase 3: Webview Core (Lit)
- Add
'repositories'tab to types.ts, app.ts, state.ts - Create
repositories-view.ts— main orchestrator component - Create
repo-card.ts— repo grid cards - Create
server-metrics-lit.ts— metrics strip
Phase 4: Detail View (Lit)
- Create
repo-detail.ts— detail layout with tab bar - Create
branch-sidebar-lit.ts— branch list + actions - Create
git-graph-lit.ts— SVG graph with lane legend + clone dropdown - Create
commit-list-lit.ts— flat commit list with expandable diff - Create
file-browser-lit.ts— grouped file tree - Create
commit-detail-panel-lit.ts— slide panel - Create
context-menu-lit.ts— shared right-click menu
Phase 5: Clone Integration
- Wire clone dropdown in git-graph to extension host
- Implement smart detection (scan workspace git remotes)
- Test full flow: discover repo → clone → add to workspace
Phase 6: Polish
- VSCode theme CSS variable integration
- Loading states, error states, empty states
- Extension
package.json: add commandvitto.showRepositories - Test with different role tiers
Risks and Mitigations
| Risk | Impact | Mitigation |
|---|---|---|
esbuild can't resolve @libs-structural/git-operations imports |
Blocks all util reuse | Verify path alias in esbuild.mjs resolves correctly. Fallback: copy utils into extension src. |
SVG innerHTML in Lit triggers CSP violation |
Graph won't render | Use unsafeHTML directive from lit/directives/unsafe-html.js. CSP nonce already configured in main-panel.ts. |
navigator.clipboard.writeText fails in webview |
Copy buttons broken | VSCode webviews support clipboard API. Fallback: postMessage to host, host does vscode.env.clipboard.writeText(). |
| Large repo list (50+ repos) slows QuickPick | Poor UX for executives | Add server-side pagination to /ai-api/git/repos. For MVP, 50 repos is fine. |
| JWT missing spaceId claim (old tokens) | User-tier filtering fails | Controller returns 401 with clear message: "Please sign out and sign in again." |
Out of Scope
- Creating repos from the extension (that's Vitto Studio workflow)
- Branch merge conflict resolution (too complex for a sidebar panel)
- File editing from the extension (read-only file viewer)
- Real-time updates via WebSocket (polling on tab focus is sufficient)
- Status bar repo indicator (nice-to-have, separate PR)
Verification Criteria
- User with
developerrole sees all repos across all spaces - User with
userrole sees only their space's repos - Git graph renders correctly with lane colors and merge curves
- Clone dropdown copies URLs and triggers VSCode native clone
- Smart detection finds already-cloned repos in workspace
- Branch CRUD works for authorized roles
- File browser groups files by directory with template filtering
- Commit list expands to show diff stats
- Server metrics strip appears only for executive/developer
- All UI respects VSCode light/dark/high-contrast themes